这个课程不是Codevolution的,而是Ali Alaa这个人的。

Installation

参考:https://tanstack.com/router/latest/docs/framework/react/installation/manual#using-file-based-route-generation

创建项目:pnpm create vite

安装依赖:

配置Vite插件:

image-20251225095630914

安装tailwind:pnpm add tailwindcss @tailwindcss/vite

配置tailwind:

image-20251225100531068

Code Based Router

TanStack Router 的 code-based router(代码式路由)结构其实非常直白,就是手动用 createRootRoute + createRoute 构建一棵路由树,最后通过 createRouter 创建路由器实例。

官方其实强烈推荐使用 file-based routing(文件系统路由),但如果你就是想用纯代码方式(比如小项目、动态路由特别多、或不喜欢插件生成代码),就可以使用Code based router。

两种方式虽然实现方式不同,但最终形成的routeTree结构是一样的,都要有一个rootRoute。

二者不同:

特性code-basedfile-based
根路径写法path: '/'index.tsx 或 routeTree根
组织方式手动 .addChildren()文件夹 + 插件自动生成
类型安全强度很好,但需要自己维护极强(插件生成最完整类型)
代码分割需手动 lazy: () => import()自动(推荐)
推荐场景非常动态路由、测试、极小项目绝大多数真实项目(官方强烈推荐)
维护成本随着路由增多越来越痛苦几乎恒定

参考文档:https://tanstack.com/router/latest/docs/framework/react/routing/code-based-routing

可以看到,路由跳转正常,使用devtools可以帮助我们跳转路由,并且展示路由树。

 

除了rootRoute之外,都使用createRoute方法来创建,必须设置getParentRoute参数。

image-20251225102812602

Navigation Links

使用Link组件来进行路由跳转。

当前路由菜单高亮有多种方法来做,因为当前路由会添加一个active的类名,所以这里直接使用tailwind来做了。

image-20251225140925891

路由跳转正常。

File Based Router

核心工作原理:

你按照特定命名约定写文件 → 构建工具插件(比如说vite插件)(或 CLI)扫描目录 → 自动生成一份类型安全的 routeTree.gen.ts → 你在入口只引入这一行就拥有完整路由树。

我们只需要做第一步:按照约定写文件。

例子:

常用的几种特殊文件名约定:

文件名写法对应路径说明常用场景
index.tsx当前层级根路径经典约定列表页、首页
route.tsx当前层级根路径显式写 route 时优先(更清晰)想放很多同文件时
$param.tsx/:param动态参数用户详情、文章页
$/*贪婪匹配(splat)404、catch-all
_layout.tsx布局路由只渲染自己 + children 的 侧边栏、顶部导航
dashboard.tsx/dashboard(不嵌套)下划线 _ 打断父级嵌套关系特殊页面跳出布局
users._index.tsx/users (不嵌套)父目录加 _ 断开嵌套独立页面
posts._$slug.tsx/posts/:slug (不嵌套)组合用法

基本使用

在src下插件routes文件夹,然后创建文件,@tanstack/router的vite插件,会帮助我们自动生成范例代码,我们只需要进行相应修改即可,太方便了:

下一步就简单了,将之前定义好的组件内容粘贴过来即可,我粘贴root过来示范:

在ruotes里面创建了文件之后,在src文件夹中会自动生成routeTree.gen.ts文件,这是插件帮助我们自动生成的,里面有一个routeTree暴露出来。

然后在main.tsx里面引入routeTree来使用。

image-20251225144046245

可以看到,路由正常。

Nesting Routes

只要文件放在某个目录下,默认就会成为该目录对应路由的子路由。这是tanstack/router的基本约定。

这节课讨论两种情况。

1、index.tsx和route.tsx的区别

image-20251225145625076

index.tsx

当一个文件夹里面是index.tsx文件时,表示该目录的「默认/索引」页面,目录路径精确匹配时才会显示这个页面。

image-20251225150435376

可以看到,生成的routeTree里面,有三个路由地址。

route.tsx

表示该目录的「主路由定义文件」(通常用来做 layout),非常适合做父级,能自然包含所有子路由。

要结合<Outlet />一起使用。

image-20251225150753210

可以看到,生成的routeTree里面,有两个路由地址。

快速决策表(开发中最常用的选择指南)

你的需求推荐写法为什么?
这个目录只需要一个页面只放 index.tsx最简单、最直观
这个目录需要布局(侧边栏、顶部导航等)route.tsx + 布局逻辑清晰,所有子页面自动共享
想要布局 + 自己的默认内容页route.tsx(布局) + index.tsx(内容)不推荐这样写,会造成冲突,推荐使用_layout.tsx + index.tsx
目录里已经有 route.tsx,还想加默认页加 index.tsx正常工作(只要不冲突)
同时放了 index.tsx 和 route.tsx 报错了删除其中一个,或改用 _layout.tsx路由生成器不允许同一个路径有两种定义文件

2、文件名或目录名后面加_

Route segments with the _ suffix exclude the route from being nested under any parent routes.

表示断开嵌套关系。注意:不是路由地址断开嵌套,而是页面内容断开嵌套。

文件名或目录名后面的下划线是一个“解耦”工具。它允许你保持 /parent/child 这样的优雅 URL 结构,同时又能在 UI 层面灵活地摆脱父级组件(Layout)的束缚。

路由文件URL 路径是否渲染在 dashboard 布局内
dashboard/index.tsx/dashboard
dashboard/profile.tsx/dashboard/profile
dashboard_/billing.tsx/dashboard/billing否(独立全屏渲染)

image-20251225153505871

注意看,路由地址在settings之间改变,但是settings/login页面的内容就不会被settings文件夹里面的任何内容影响。

解决 TanStack Router 所有“隐式 any”问题的终极方案。你需要确保在项目的入口文件(如 main.tsxApp.tsx)中添加了类型声明。加了这段声明之后,可以省很多心。

Pathless Layouts

在 TanStack Router 中,前缀下划线(例如 _layout.tsx_auth/)被称为 Pathless Route(无路径路由)

它的核心作用是:为了逻辑分组或 UI 嵌套,但不在 URL 路径中显示。

1. 核心定义

当你给文件夹或文件加上 _ 前缀时,你是在告诉路由器:

2. 实际案例对比

假设你的需求是:/login/register 页面需要共享一个蓝色的背景和公司的 Logo。

❌ 不使用前缀的情况

如果你的结构是:

生成的 URL/auth/login/auth/register

✅ 使用前缀 _ 的情况

如果你的结构是:

生成的 URL/login/register。 虽然 URL 里没有 auth,但这两个页面都会自动嵌套在 _auth.tsx<Outlet /> 中。

3. 三大使用场景

① 视觉布局分组 (Visual Grouping)

这是最常见的用法。你可能希望“营销页”用一套导航栏,“管理后台”用另一套导航栏,但 URL 都想从根目录开始(如 /home/dashboard)。

② 逻辑与权限控制 (Auth/Logic Wrappers)

你可以创建一个 _authenticated.tsx 文件,在里面编写重定向逻辑:如果用户未登录,则跳转到登录页。所有放在 _authenticated/ 文件夹下的路由都会自动受到这个逻辑的保护,而 URL 中不会多出 /authenticated/ 这一层。

③ 纯粹的文件组织

当你的项目非常大时,你可能只想把相关的页面放在一起管理,但不希望改变现有的 SEO 友好的 URL 结构。

image-20251225155423581

注意看,路由地址是很简洁的,同时也有_auth.tsx文件的内容。

 

pathless route不只是可以省略一段url path,更重要的是共享一段逻辑或样式。

如果单纯的只是想要省略一段url path,那么可以使用(folder),小括号来实现。

Ignored Folders

- Prefix

有时候,我们想就近编写一些组件,而不是都放到src/components这个文件夹里面。但是一个文件夹就是一个路由地址,该怎么办才能让tanstack/router忽略这个文件夹呢?

可以在文件夹或者文件名前面加上-,这样就会排除出路由树了。

Files and folders with the - prefix are excluded from the route tree. They will not be added to the routeTree.gen.ts file and can be used to colocate logic in route folders.

image-20251225160743125

image-20251225160800986

可以看到,Nav组件正常使用了。

(folder) folder name pattern

A folder that matches this pattern is treated as a route group, preventing the folder from being included in the route's URL path.

路由分组。它纯粹是为了组织文件,完全不影响路由的层级。

核心作用

符号位置官方术语作用
_前缀Pathless Layout隐藏路径,但保留 UI 布局嵌套
_后缀Non-nested Route保留路径,但跳出父级 UI 布局
()包裹Route Group隐藏路径,且不产生 UI 布局嵌套
.中间Flat Route扁平化文件命名,代替多层文件夹

Flat Routes

在 TanStack Router 中,Flat Routes (扁平路由) 是一种通过文件名中的特殊分隔符(如 .)来表达路由层级,而不是依赖物理文件夹层级的方案。

它是 TanStack Router v1.0 之后的推荐趋势。

1. 什么是 Flat Routes?

在 Flat Routes 模式下,你不需要一层套一层的文件夹。你直接在 routes/ 根目录下创建文件,用点 . 来代表路径分隔符。

对比示例:

2. 扁平路由用的多吗?

非常多,甚至正在成为主流。

TanStack Router 的官方 CLI 工具和示例代码现在默认倾向于使用这种扁平化结构。原因在于:

  1. 文件查找极快:所有路由文件都在一个列表里,一眼就能看到项目有多少个页面,不需要反复展开/折叠多层文件夹。
  2. 避免“Index 泥潭”:在文件夹模式下,你会有一堆名为 index.tsx 的文件,在编辑器标签页里很难分辨。扁平模式下,文件名就是完整路径(如 dashboard.settings.tsx),非常清晰。

3. Flat Routes vs. 文件夹模式:深度对比

维度文件夹模式 (Directory)Flat Routes (扁平)
可读性适合深度嵌套的超大型项目适合大多数项目,路径清晰
开发体验编辑器里会出现很多 index.tsx每个文件名都是唯一的,好搜索
移动文件移动文件夹会影响一大片路由修改文件名即可重构路由,风险低
代码组织可以把组件、样式放在同名文件夹下路由文件只存路由,组件建议放外面

4. 哪个更好一些?

这取决于你的代码组织习惯

选 Flat Routes (推荐) 如果:

选 文件夹模式 如果:

5. 混合模式 (Hybrid)

实际上,TanStack Router 支持混合使用。你可以在 routes/ 下用扁平化命名处理简单路由,对于复杂的模块(比如有大量私有组件的 Dashboard),再开一个文件夹。

总结建议

对于新项目,强烈建议优先使用 Flat Routes。它配合你之前问的 _(前缀/后缀)能组合出非常灵活的方案:

Dynamic Path Segments

Route segments with the $ token are parameterized and will extract the value from the URL pathname as a route param.

在 TanStack Router 中,Dynamic Path Segments(动态路径段) 是处理 URL 变量(如 ID、用户名等)的核心机制。它允许你通过定义通配符来匹配不同的路径。

在文件路由中,动态段的主要标志是 $ 符号。

1. 基础语法:使用 $

当你希望 URL 中的某一部分是变动的时候,就在文件名或文件夹名中使用 $

2. 在代码中获取参数

定义了动态路由后,你需要通过 TanStack Router 提供的 Hook useParams 来获取具体的值。

3. 三种常见的动态段类型

① 单个段 (Single Segment)

② 多个段 (Multiple Segments)

4. 动态段与类型安全(TanStack Router 的杀手锏)

与其他路由库(如 React Router)不同,TanStack Router 的动态段是全自动类型化的。

5. 进阶:在 Loader 中使用动态段

动态段最常用的场景是在 loader 中获取数据。例如根据 ID 从 API 请求数据:

6. Link标签使用params参数传参

Link的to参数,还是写成定义的路由地址那样/posts/$postId,使用params来传递参数。

上面这种方式主要是为了类型安全,也可以直接在to参数上拼接来传参。

Catch-All Routes

在 TanStack Router 中,Catch-All Routes(全匹配路由 / 通配符路由) 用于匹配 URL 中剩余的所有路径部分。当你无法预知路径有多少层级,或者想要捕获所有错误的路径时,这个功能非常有用。

在文件路由中,Catch-All 的核心标志是单个美金符号 $(不带任何名称)。

1. 语法与命名规则

在扁平路由(Flat Routes)或文件夹路由中,你只需要将文件命名为 $.tsx

2. 如何获取匹配到的路径值?

由于 Catch-All 匹配的是“剩余的所有部分”,TanStack Router 会将匹配到的路径以字符串的形式存储在 params 中,属性名固定为 _ (下划线)。

3. Catch-All 的常见用途

① 404 页面(NotFound)

这是最常见的用途。在 routes/ 根目录下创建一个 $.tsx,它会捕获所有无法匹配到已有路由的请求。

② 层次结构不固定的文件管理器

如果你在做一个类似百度网盘或 GitHub 的文件浏览器,路径深度是动态的:

4. 优先级与匹配规则

TanStack Router 的路由匹配遵循 “最具体优先” 原则:

  1. 静态路由 (routes/about.tsx) 优先级最高。
  2. 动态段 (routes/posts.$id.tsx) 优先级次之。
  3. Catch-All (routes/$.tsx) 优先级最低,只有在前两者都匹配失败时才会被触发。

Optional Parameters

在 TanStack Router 的 file-based routing 中,{-$locale}.archive.{-$year}.{-$month}.{-$day}.tsx这种写法属于 非常典型的“多段动态路径 + 可选参数” 的文件命名方式。它通常用于实现带日期和语言的博客/文章/新闻归档路由,是非常常见的真实项目写法。

这个文件名到底匹配什么 URL?它会匹配下面这种结构的 URL:

简单说就是:

各部分具体含义

写法含义是否必须在 URL 中的表现取参方式
{-$locale}可选的语言/地区前缀可选最前面一段(如果有)useParams().locale
archive固定的路径段(字面量)必须永远是 /archive
{-$year}4位年份(通常是数字)可选年份那一段useParams().year
{-$month}2位月份可选月份那一段useParams().month
{-$day}2位日期可选日期那一段useParams().day

几种常见的实际匹配例子

URL 例子能匹配到这个文件吗?得到的 params 对象大概是
/archive/2025/12/25{ year: "2025", month: "12", day: "25" }
/en/archive/2024/10/01{ locale: "en", year: "2024", ... }
/zh-hans/archive/2023/08/19{ locale: "zh-hans", year: "2023", ... }
/archive/2025通常不会(因为后面还缺 month 和 day)
/archive/2025/12通常不会(缺 day)
/fr/blog/2025/12/25不会(archive 不匹配)

为什么前面要加 -?(非常重要的细节)

TanStack Router 的动态参数有两种写法:

- 的作用就是让这个路径段变成可选。所以 {-$locale}.archive.{-$year}.{-$month}.{-$day}.tsx 的意思是:

前面的语言和后面的年/月/日 都可以不存在,只要中间有 /archive 就行(但实际项目里通常会搭配 loader 来做更严格的验证)

范例代码:

Search Parameters

在 TanStack Router 中,Search Parameters(查询参数,即 URL 中 ? 后面的部分) 被提升到了“一等公民”的地位。

与传统的路由库不同,TanStack Router 要求你对查询参数进行显式验证和类型定义。这意味着你可以像处理组件的 Props 一样,以类型安全的方式处理 URL 参数。

1. 核心流程:验证与定义

要使用 Search Params,你必须在定义路由时使用 validateSearch 函数。

2. 为什么这样做?(三大优势)

3. 如何在代码中使用

获取参数:useSearch

修改参数:useNavigate<Link>

改变 Search Params 通常用于分页、排序和搜索过滤。

4. Search Params vs Path Params

特性Path Params ($id)Search Params (?page=1)
定位标识资源本身 (Identity)改变资源的呈现方式 (State)
必要性通常是必须的通常是可选的或有默认值
类型总是字符串可以通过验证器转为数字、布尔、对象等
SEO权重高,路径清晰权重较低,适合过滤和排序

5. 进阶:JSON 状态压缩

TanStack Router 支持将复杂的对象甚至数组放入 Search Params。它会自动处理序列化和反序列化,让你可以直接在 URL 中存储像 ?filters={"status":["active","pending"]} 这样的复杂状态。

例子:

总结

在 TanStack Router 中,Search Params 不再是“杂乱的字符串”。通过 validateSearch,你把 URL 变成了一个类型安全的全局状态管理器

Validating Search Params with Zod

虽然可以自己写search params的校验逻辑,但是结合zod使用就非常高效。

安装依赖pnpm add @tanstack/zod-adapter

在 TanStack Router 中使用 Zod 校验 Search Params,本质上是把 URL 字符串 变成 结构化的 TypeScript 对象

当你访问 ?page=1&filter=react 时,浏览器只知道它们是字符串。Zod 的作用就是解析、转换并确保这些数据的安全。

一个典型的校验逻辑拆解,比如说url是这样的/search?page=1&criteria={"query":"laptop","range":{"min":1000,"max":5000}}&tags=electronics&tags=sale

第一步:定义zod schema

常见转换方法:

Zod 方法处理 URL 时的作用
z.coerce.number()把字符串 "10" 变成数字 10
z.coerce.boolean()把字符串 "true" 变成布尔值 true
.default(val)如果 URL 没传这个 key,就填入默认值
.catch(val)最推荐。如果用户传错了,就填入默认值,防止页面挂掉
.transform()进一步处理数据,比如把 "2024-01-01" 转为 Date 对象

第二步:配置路由与 Loader

在这里,Loader 会直接拿到经过 Zod 清洗后的数据,直接发送给 API。

可以看到,validateSearch里面就很简单了。

第三步:组件内的交互

展示如何使用 useSearch 获取状态,以及如何通过 useNavigate 修改状态。

  1. 这个“复杂”例子的核心亮点
  2. 数据过滤逻辑高度一致:由于采用了 validateSearch,用户无论是在搜索框输入、点击分页、还是点击浏览器后退按钮,页面拿到的 search 对象格式永远是一致且安全的。
  3. Loader 自动触发:只要 URL 里的 pagefilter 变了,TanStack Router 就会自动重新调用 Loader 获取新数据。你不需要写任何 useEffect 来监听参数变化。
  4. URL 是唯一的真理 (Single Source of Truth):整个页面的过滤、排序状态完全由 URL 决定。你可以直接把当前的链接发给同事,他打开看到的页面和你一模一样。
  5. Zod 的预处理 (preprocess):在处理布尔值(如 showArchived)时,URL 里是字符串 "true",Zod 自动将其转成了 JavaScript 的 true

Zod 在路由生命周期中的位置

TanStack Router 保证了在你的组件渲染之前,数据就已经通过了 Zod 的检查。

Handling Search Params Errors

如果用户输入了错入的url地址,该怎么处理呢?

在 TanStack Router 中,Zod 的错误处理主要分为两个层面:Schema 内部的“自动纠错”和路由层面的“全局拦截”

1. 第一道防线:Zod 内部的“软处理” (推荐)

这是最常用的方式,通过在 Schema 中定义“后路”,确保即使用户输入错误,应用也不会挂掉。

A. 使用 .catch() (最强纠错)

无论发生什么错误(解析失败、类型不符、为空),都返回一个指定的默认值。

B. 使用 .default() (处理缺失)

只在 URL 中缺少该参数时生效。

C. 结合使用

通常建议这样写,既能处理缺失,也能处理错误:

D. 使用fallback处理

fallback 函数通常是从 @tanstack/react-router 中导出的,专门用于 validateSearch 阶段。

fallback,catch二者选择一种就行了,千万不要混着使用,因为二者效果是一致的,混着使用代码可读性非常差。

2. 第二道防线:路由层面的“硬拦截”

如果你没有在 Zod Schema 中写 .catch(),那么当 Zod 校验失败(.parse() 抛出异常)时,TanStack Router 会启动错误流程。

A. 触发 errorComponent

如果 validateSearch 抛出错误,该路由的 errorComponent 会被渲染。你可以在这里给用户展示一个“搜索条件错误”的提示。

B. 拦截重定向

你也可以在校验失败时,强行把用户踢回正确的 URL。

3. Zod 错误处理的底层逻辑图

  1. 用户修改 URL -> 2. validateSearch 执行 -> 3. Zod 校验

    • 成功 -> 进入 loader -> 渲染组件。
    • 失败 (有 .catch) -> 修正数据 -> 进入 loader -> 渲染组件。
    • 失败 (无 .catch) -> 抛出异常 -> 寻找最近的 errorComponent 或跳转。

4. 进阶:如何自定义 Zod 报错信息?

有时候你想在 UI 上精确显示哪个字段错了,可以利用 Zod 的 errorMap 或者简单的 try-catch

5. 总结:最佳实践建议

  1. 对于分页、排序、筛选:一律使用 .catch()

    • 原因:用户手动改 URL 很正常,不应该因为一个参数错了就让整个页面崩溃(显示报错页面)。让他们回到默认状态是最好的体验。
  2. 对于关键性业务参数:不使用 .catch(),让它抛错。

    • 原因:比如一个加密的校验 Token,如果错了,就应该明确显示“链接已失效”。
  3. 使用 zodValidator:它会自动处理 Zod 的 ZodError 对象,并将其传递给 errorComponent,让你能拿到详细的错误数组(哪些字段错了,原因是什么)。

More on the Link Component

1. 强类型的参数检查 (Type-Safe Params & Search)

这是 TanStack Router 最核心的优势。如果你定义的路由有动态参数或查询参数,<Link> 会强制要求你提供它们,否则代码无法通过编译。

2. 智能预加载 (Intent-based Preloading)

TanStack Router 可以在用户真正点击链接之前就加载好数据(Loader)。

效果:用户点击时,数据通常已经加载好了,页面秒开。

3. 函数式更新 Search Params

当你只想修改当前 URL 的一个查询参数(例如分页),而不影响其他参数时,可以使用函数写法。这避免了手动合并复杂的 search 对象。

4. 动态 Active 状态处理

你可以根据链接是否处于激活状态(Active)来应用不同的样式或组件。

5. Data 属性与 View Transitions

你可以配合浏览器原生的 View Transitions API 实现平滑的页面过渡效果。

6. 遮罩跳转 (Masking URL)

有时候你想让用户跳转到某个路由,但在地址栏显示另一个更美观的地址。

总结:Link 的高级特性对比

特性作用带来的好处
Type Safety编译时检查 to, params, search减少运行时错误,重构更放心
Preloadinghover 时预取数据极致的性能体验,消除加载白屏
Search Function(prev) => ({...})简化复杂筛选、分页的逻辑
Active States内置 isActive 逻辑轻松实现复杂的导航 UI

如果我使用了tanstack/router,也使用了tanstack/table,我该怎么处理分页和搜索呢?

将 Table 的状态(分页、排序、过滤)“同步”到 URL 中。url长度是有限的,只要不超过这个限制,就没有问题。

不要在表格组件里使用 useState 来维护 pageIndexglobalFilter,而是:

  1. 从 URL 读取状态(通过 useSearch)。
  2. 将状态传给 Table(作为 state 属性)。
  3. 在 Table 变化时更新 URL(通过 onStateChange 触发 Maps)。

Other ways to Navigate Programmatically

1. 使用 useNavigate Hook (最常用)

这是在组件内部进行逻辑跳转的标准方式。它返回一个 Maps 函数,支持与 <Link> 完全相同的类型安全参数。

高级技巧: 配合 from 属性,你可以像 Link 一样实现 Search Params 的函数式更新。因为from属性指定之后,tanstack会记住当前路由的参数。

2. 在 Loader 或 BeforeLoad 中使用 throw redirect

如果你需要在页面渲染 之前 进行拦截并跳转(例如:未登录用户访问后台),你应该使用 redirect 函数。

3. 使用 router.navigate (全局 API)

如果你在组件外部(例如在 Redux 中、全局拦截器中或普通 JS 函数里),可以通过直接访问 router 实例来进行跳转。

4. 路由跳转方式对比

方法使用场景特点
<Link>用户点击 UI 元素支持预加载 (Preload),SEO 友好,首选方式。
useNavigate事件回调、异步逻辑完成后编程式,支持所有 Link 的类型检查特性。
throw redirectloaderbeforeLoad 期间在渲染前拦截,常用于权限控制。
router.navigate组件外部、全局单例适用于非 React 上下文。

5. 跳转时的“姿势”控制

在调用跳转方法时,你可以通过一些特殊的属性来控制跳转行为:

其实还有一个<Navigate to="">组件可以用来跳转,但是这个组件有一个问题,它是当组件被渲染时,立即触发路由跳转。可能会造成页面闪动、白屏的情况,使用时需注意。

Context

在 TanStack Router 中,Context(上下文) 是一个非常强大的特性,它允许你在路由树中自上而下地传递数据、服务或状态(例如:QueryClient、Auth 信息、主题等)。

1. 核心流程:从根路由开始

Context 的传递通常是从 rootRoute 开始定义的。

第一步:定义 Context 类型

首先,你需要定义一个类型来描述你的上下文包含什么。

image-20251226154920342

第二步:在创建 Router 时注入具体值

在你的 main.tsxApp.tsx 中,你需要把真实的数据传给 createRouter

2. 在子路由中消费 Context

子路由可以通过 beforeLoadloader 访问到这些 Context。

3. Context 的“继承与合并”

Context 是可以累加的。父路由可以向下传递 Context,子路由也可以在自己的 beforeLoad 中添加新的 Context 给它的子路由。

4. 在组件中获取 Context

如果你需要在 UI 组件中访问 Context,可以使用 useContext 钩子(注意:这是 Route 对象自带的钩子)。

image-20251226155832031

5. 常见实战场景:集成 TanStack Query

这是 Context 最常见的用法。通过将 queryClient 注入 Context,你可以在所有路由的 loader 中预取数据。

TypeScript

6. 为什么不用 React Context?

虽然它们名字一样,但 Router Context 有几个独特的优势:

  1. 非渲染时机访问:React Context 只能在组件渲染时(Hooks)使用,而 Router Context 可以在路由跳转前(loader / beforeLoad)使用。
  2. 类型安全:通过 createRootRouteWithContext,整个路由树的 Context 都是强类型的,IDE 会全程提供补全。
  3. 数据流清晰:它迫使你思考哪些数据是路由级别的(权限、数据客户端),哪些是 UI 级别的。

总结

Passing State in Context

在 TanStack Router 中“传递状态到 Context”:

  1. 定义:在 RootRoute 定义接口。
  2. 注入:在 App 组件中将状态传递过去。
  3. 消费:在子路由通过 context 对象(Loader/BeforeLoad)或 useRouteContext()(组件)访问。

1. 模式核心:在根组件中动态更新 Context

你需要在 createRouter 时提供初始结构,然后在 React 根组件渲染时,使用router.invalidate同步状态。

第一步:定义 Context 类型

image-20251226161916879

第二步:在 App 组件中注入并同步状态

这是关键点。你可以在顶层组件维护状态,并通过setState方法传递到上下文中,在子组件中可以调用setState来变更状态。

2. 在子路由中消费这个状态

现在,这个状态已经存在于路由树的 Context 中了,你可以跨过组件层级直接使用。

beforeLoad 中拦截

在组件中使用 useRouteContext 的上下文来修改状态

可以看到,login、logout按钮控制了上下文,这真是好用:

Loader Functions: Posts Page

在 TanStack Router 中,Load(加载) 是其最核心的机制。它遵循“并行加载”和“渲染前获取数据”的原则,彻底解决了传统 React 应用中常见的“瀑布流(Waterfall)”加载问题。

以下是与 Load 相关的核心方法及其应用场景:

1. beforeLoad:准入与前置逻辑

这是路由解析流程中的第一个钩子。它在 loader 运行之前执行,主要用于鉴权、重定向或向 Context 注入数据

2. loader:获取数据的主战场

这是最常用的方法。它在路由匹配时异步执行,返回的数据可以通过 Route.useLoaderData() 在组件中访问。

3. staleTime & gcTime:加载缓存控制

TanStack Router 内置了轻量级的缓存机制(虽然它也经常配合 TanStack Query 使用)。

4. shouldReload:精细化重新加载

默认情况下,任何 URL 参数变化都会触发 loadershouldReload 允许你自定义什么时候需要重新加载数据。

5. pendingComponent & pendingMs:加载状态

这是提升用户体验的神器。当 loader 运行时间超过设定值时,会自动渲染该组件。

总结:Load 相关方法的执行顺序

  1. beforeLoad:检查权限,准备 Context。
  2. loader:并行异步请求数据。
  3. pendingComponent:如果 loader 太慢,中间插播 Loading 界面。
  4. component:数据准备就绪,正式渲染。
  5. errorComponent:如果上述任何环节出错,跳转到这里。

进阶:如何获取正在加载的状态?

如果你想在全局(比如顶部进度条)显示加载状态,可以使用 useRouterState

老师的案例

创建一个posts页面,然后获取列表数据:

可以看到,跳转到posts页面之后,就成功渲染了列表,并且跳转详情成功:

我有一个问题:

在preload和loader里面,都可以获取数据,为什么老师要在preload里面暴露一个fetch接口给loader里面使用呢?有什么好处吗?

这种写法是 TanStack Router 中非常高级且推荐的依赖注入(Dependency Injection)模式。你通过 beforeLoad 将一个函数(getPosts)注入到 Context 中,然后在 loader 里消费它。

这样做主要有以下四个核心好处:


1. 实现“逻辑与路由”的解耦

这种写法允许你在路由定义之外准备好业务逻辑。

2. 极大地简化单元测试(Easy Mocking)

这是这种模式最大的优势。

3. 统一处理副作用或配置

通过 Context 注入的方法,通常已经预设好了必要的参数。

4. 解决循环依赖问题

在大型 React 项目中,API 文件夹、组件文件夹和路由文件夹之间经常发生循环引用(Circular Dependency)。


对比一下普通写法:

普通写法(直接调用):

你的图片中的写法(依赖注入):

Adding Pagination

这节课学习为列表添加分页功能。

在 TanStack Router 中,loader 默认只在路径参数(params)改变时触发。如果你想让 loader 在 URL 的 Search Params(查询参数) 改变时也重新运行,你就必须使用 loaderDeps

1. 什么是 loaderDeps

loaderDeps 是一个函数,它告诉 Router:“除了路径变化外,我还依赖哪些额外的参数”。如果这个函数返回的值发生了变化,Router 就会重新执行 loader

它通常配合 validateSearch 使用。

2. 代码示例:处理分页

假设你要根据 URL 里的 page 参数来拉取数据:

3. 为什么必须用 loaderDeps

如果没有 loaderDeps,会发生以下情况:

  1. 用户从 /posts?page=1 点击跳转到 /posts?page=2
  2. 因为 路径 依然是 /posts/ 没变,Router 为了性能优化,默认不会重新触发 loader
  3. 你的界面会停留在第一页的数据,URL 变了但内容没变。

加上 loaderDeps 后,Router 会对比旧的 deps 和新的 deps。一旦发现 page1 变成了 2,它就会立即触发加载逻辑。

4. loaderDeps 的工作流程

  1. 用户操作:点击了带有新 search 参数的 <Link>
  2. 校验validateSearch 确保参数合法。
  3. 依赖检查:Router 执行 loaderDeps,对比返回的依赖对象。
  4. 重新加载:如果依赖项不同,触发 loader
  5. 数据流向loader 拿到新的 deps,请求 API,最后通过 useLoaderData 传给组件。

5. 常见坑点:不要把整个 search 传进去

loaderDeps 中,建议只返回你真正依赖的字段,而不是返回整个 search 对象。

总结

老师案例

image-20251226191733230

 

可以看到,翻页非常顺畅。

Loading Data in Parallel: Single Blog Post Page

这节课学习平行请求。在post详情页面,同时获取post详情和相应的评论数据。

通常的做法

这样的结果是:一个接口请求了之后,才会执行下一个请求。注意看右边的Network执行情况。

平行请求

使用Promise.all来做平行请求。

可以看到,两个请求是同时发起的,节约了时间。

Pending State Component

假设接口返回很慢,这时候页面就会有明显的卡顿。parallel请求里面,只要有一个请求慢,那么整个页面都会受到影响。

可以看到,从列表页面进入的时候,只有接口返回了,才会显示详情页面。并且刷新详情页面的时候,只有接口返回了,才会显示页面,有空白页面的情况。

需要设置pending state相关的属性来解决上面的问题。

相关属性

如何既能提示用户正在加载,又能避免 Loading 界面一闪而过的“闪烁”感?

1. pendingComponent

这是你要展示的 Loading 界面 本身。

2. pendingMs

这是 “等待多久才显示 Loading” 的延迟时间。

3. pendingMinMs

这是 “Loading 界面最少显示多久”

三者结合的逻辑流

精确逻辑流:场景(假设API 耗时 300ms)

  1. 0ms - 开始加载:用户点击链接,路由进入 pending 状态,浏览器开始计时 pendingMs

  2. 200ms - 触发显示:此时 API 还没返回(还差 100ms)。因为达到了 pendingMs 的阈值,pendingComponent(Loading 界面)正式挂载显示

    • 关键点pendingMinMs500ms 倒计时从这一刻正式开始
  3. 300ms - 数据返回:API 请求完成,数据已就绪。

    • 关键点:此时 Loading 界面仅显示了 100ms。由于未达到 pendingMinMs(500ms)的要求,页面被锁定在 Loading 状态。
  4. 700ms - 结束停留:Loading 界面已经显示了满 500ms(200ms 触发 + 500ms 持续时间)。

  5. 正式渲染:切换为正式的 RouteComponent

结论:在这种情况下,用户从点击到看到页面,总共等待了 700ms

老师案例

image-20251226200448657

可以看到,loading...效果是过了一段时间(设置的是300ms)才出现的,出现之后就会持续500ms的时间,所以这些数值要根据实际情况来。

全局pending属性

全局 defaultPendingComponent 的默认效果是替换掉整个页面内容。所以这个还是要看需求。

image-20251226202205963

问题:

我发起parallel request请求,如果有一个接口延迟比较久的话,整个页面都会延迟显示,虽然有pending状态可以处理,但是能不能像nextjs里面的parallel routes那样处理呢?

将不同请求作为不同的组件,然后引入到这个route页面里面。然后每个组件里面都有独立的loading状态,互不影响。

TanStack Router 中,如果你想实现类似 Next.js 那种“局部加载、局部占位”的效果,不要在 loader 中等待所有的请求

loader 的特性是:它必须执行完毕,页面才会从 pending 状态转入正式组件。如果你在 loader 里写了 await Promise.all([A, B]),那么 A 和 B 中最慢的那个会拖慢整个页面的首屏显示。

要实现并行且独立的局部加载,主要有以下两种方案:

方案一:利用 Suspense 与“延迟数据”(最接近 Next.js)

TanStack Router 支持在 loader 中返回一个不加 await 的 Promise。这样路由会立即渲染,而数据获取则交给组件内部的 Suspense 处理。

1. 在 Loader 中分发请求

2. 在组件中局部占位

使用 Await 组件(TanStack 提供的工具)来包裹那些慢速请求:


方案二:组件内独立请求(配合 TanStack Query)

如果你希望不同组件完全独立,互不干扰,甚至不需要在 loader 里声明,那么将请求直接移入子组件是最佳选择。

  1. Loader 只处理关键路径数据(如鉴权、核心 ID)。
  2. 子组件内部发起请求

Loading Data in the Background

上节课我就提出了疑问,因为一个接口而让整个页面都显示loading,效果其实也不好,当时有两种解决办法,这里就说第一种解决办法:<Await>组件。

The Await component is a component that suspends until the provided promise is resolved or rejected. This is only necessary for React 18. If you are using React 19, you can use the use() hook instead.

虽然说现在是react 19,可以使用use()来解决了。但是Await组件用起来更加语义化一些,更加好理解。

基本概念

在 TanStack Router 中,<Await> 组件是实现非阻塞式数据加载(Deferred Data)的核心工具。它允许你在 loader 中发起异步请求但不等待其完成,从而让页面框架先渲染,数据则在后台加载完成后局部显示。

1. 为什么需要 <Await>

在默认情况下,如果你在 loader 里使用 await,Router 必须等数据加载完才会渲染组件。如果接口很慢,用户就会卡在旧页面或看到全局的 pendingComponent

<Await> 的作用:它接收一个 Promise,并利用 React 的 Suspense 机制,在 Promise 没完成时展示占位图(fallback),完成后自动渲染内容。

2. 核心用法流程

第一步:在 loader 中返回 Promise(不加 await

不要在慢速请求前写 await,直接将 Promise 对象返回。

第二步:在组件中使用 <Await> 包裹

你需要将组件包裹在 Suspense 中,因为 <Await> 会触发“悬停”(suspend)。

3. <Await> 的核心属性

属性类型说明
promisePromise<T>必须传入一个正在进行的异步任务。
children(data: T) => ReactNode一个函数(Render Child),其参数就是 Promise 解析后的结果。

4. 进阶:错误处理

如果 promise 失败了,<Await> 默认会抛出错误并触发最近的 ErrorBoundary。你可以配合 RouteerrorComponent 使用,或者在局部使用自定义错误边界。

老师案例

image-20251226205329840

image-20251226205350976

 

可以看到,post详情页面先显示了,comments里面显示loading效果,等到comments接口返回了,就显示。效果和我预想的一致,很好。